<?php
include_once BXD_INCLUDEPATH . 'lib/entity/EntityManagerMini.php';
/**
 * Pattern Singleton per eseguire le query sotto transazione operando nello stesso
 * modo con cui si eseguono le query con l'EntityManger
 *
 * @author Damiano Ciarla
 * @version 1.0
 * @category lib/entity
 */
class EntityManagerTransaction extends EntityManagerMini
{
	/**
	 * Costante per definire un valore NULL nelle query di INSERT e UPDATE
	 */
	const NULL_VALUE = "BXD_NULL_VALUE";

	/**
	 * Questa variabile conterrà l'istanza della classe EntityManagerTransaction dopo
	 * essere stata creata dal metodo getInstance()
	 *
	 * @var EntityManagerTransaction Istanza della classe EntityManagerTransaction
	 */
	private static $instance = NULL;

	private $openTransaction = FALSE;
	private $runtimeError = FALSE;

	/**
	 * Costruttore della classe EntityManagerTransaction il quale richiama il costruttore
	 * della classe EntityManagerMini per inizializzare le variabili di default su cui lavorare
	 *
	 * @param String $dbName Nome del database su cui operare. Di default il file di config è "DBDefault"
	 */

	public function __construct($dbName)
	{
		parent::__construct($dbName);
	}

	/**
	 * Metodo che restituisce l'istanza della classe EntityManagerTransaction qualora
	 * questa non fosse già stata creata
	 *
	 * @param String $dbName Nome del database su cui operare. Di default il file di config è "DBDefault"
	 * @return EntityManagerTransaction Istanza della classe EntityManagerTransaction
	 */

	public static function getInstance($dbName = NULL)
	{
		if (self::$instance == null)
			self::$instance = new EntityManagerTransaction($dbName);

		return self::$instance;
	}

	/**
	 * Metodo utilizzato per aprire una transazione
	 *
	 * @param boolean $execRuntimeQuery Parametro che imposta se eseguire la query nel momento in qui viene
	 * inserita in transazione o eseguirle nel metodo di chiusura
	 */

	public function transactionStart($execRuntimeQuery = TRUE)
	{
		if($this->openTransaction)
		{
			BXD::newException('TransactionError', 'Transaction already STARTED. Execute the method transactionClose() and tray again.');
		}
		else
		{
			$this->openTransaction = TRUE;
			$this->runtimeError = FALSE;
					
			$this->db->beginTransaction(); //--> Apertura della transazione
		}
	}

	/**
	 * Metodo che esegue un DELETE nel Database prendendo in ingresso un oggetto ENTITY
	 * sul quale viene costruita la query servendosi solamente dei campi valorizzati e
	 * ignorando quelli NULL. 
	 *
	 * @param Entity $entity Oggetto Entity da inserire nel DB
	 *
	 * @example
	 *
	 *	Esempio di come utilizzare il metodo delete per eliminare una semplice entity con valore ID
	 *
	 *	$EntityUsers = new EntityUsers();
	 *	$EntityUsers->setUserId(1);
	 *
	 *	EntityManagerTransaction::getInstance()->delete($EntityUsers);
	 */

	public function delete(Entity $entity)
	{
		if($this->openTransaction)
		{
			$query = "DELETE FROM " . $entity->_getTableName() . " WHERE ";

			$serial = $entity->_getSerial();

			if($serial !== NULL) // esiste una chiave primaria autoincrement
			{
				$query .= "`" . $serial["DB_COLUMN"] . "` = " . $entity->__get($serial["VAR_NAME"]);
			}
			else // usiamo il pattern per il riconoscimento delle tuple da aggiornare
			{
				$count = 0;

				foreach($entity->_getVars() as $name => $col)
				{
					$val = $entity->__get($name);

					if(isset($val))
					{
						$value = "";

						switch($col['TYPE'])
						{
							case EntityType::BIT:
							case EntityType::TINYINT:
							case EntityType::SMALLINT:
							case EntityType::MEDIUMINT:
							case EntityType::INT:
							case EntityType::BIGINT:
								$value = (($val === self::NULL_VALUE) ? "NULL" : intval($val));
								break;

							case EntityType::FLOAT:
							case EntityType::DOUBLE:
							case EntityType::DECIMAL:
								$value = (($val === self::NULL_VALUE) ? "NULL" : "'" . addslashes($val) . "'");
								break;

							case EntityType::CHAR:
							case EntityType::VARCHAR:
							case EntityType::TEXT:
							case EntityType::MEDIUMTEXT:
							case EntityType::LONGTEXT:
							case EntityType::ENUM:
								$value = (($val === self::NULL_VALUE) ? "NULL" : "'" . addslashes($val) . "'");
								break;

							default:
								$value = (($val === self::NULL_VALUE) ? "NULL" : "'" . addslashes($val) . "'");
								break;
						}

						if($count == 0)
						{
							$query .= "`" . $col['DB_COLUMN'] . "` " . (($val === self::NULL_VALUE) ? "IS" : "=") . " " . $value;
						}
						else
						{
							$query .= " AND `" . $col['DB_COLUMN'] . "` " . (($val === self::NULL_VALUE) ? "IS" : "=") . " " . $value;
						}

						$count++;
					}
				}
			}
			
			$result = $this->db->exec($query);

			if($result === FALSE)
			{
				$this->runtimeError = TRUE;
				$this->transactionClose();

				BXD::newException('TransactionError', 'Error execute query: ' . $query);
			}
		}
		else
		{
			BXD::newException('TransactionError', 'Transaction not STARTED. Execute the method transactionStart() and tray again.');
		}
	}

	/**
	 * Metodo che esegue una INSERT nel Database prendendo in ingresso un oggetto ENTITY
	 * sul quale viene costruita la query servendosi solamente dei campi valorizzati e
	 * ignorando quelli NULL. Una volta effettuata l'operazione di INSERT il metodo si
	 * occupa di valorizzare il campo ID (se esiste ed è autoincrement) con l'ultimo
	 * inserito nella tabella
	 *
	 * @param Entity $entity Oggetto Entity da inserire nel DB
	 *
	 * @example
	 *
	 *	Esempio di come utilizzare il metodo insert per aggiungere una semplice entity
	 *	NAME-SURNAME nella tabella USERS
	 *
	 *	$EntityUsers = new EntityUsers();
	 *	$EntityUsers->setName('Giuseppe');
	 *	$EntityUsers->setSurname('Rossi');
	 *
	 *	EntityManagerTransaction::getInstance()->insert($EntityUsers);
	 *
	 * @return int Numero di righe inserite
	 */

	public function insert(Entity $entity, $useIgnoreMethod = FALSE)
	{
		if($this->openTransaction)
		{
			$field = "";
			$value = "";
			$index = 0;

			foreach($entity->_getVars() as $name => $col)
			{
				$val = $entity->__get($name);

				if(isset($val) || $val === self::NULL_VALUE)
				{
					$field .= (($index > 0) ? ", `" : "`") . $col['DB_COLUMN'] . "`";

					switch($col['TYPE'])
					{
						case EntityType::BIT:
						case EntityType::TINYINT:
						case EntityType::SMALLINT:
						case EntityType::MEDIUMINT:
						case EntityType::INT:
						case EntityType::BIGINT:
							$value .= (($index > 0) ? ", " : "") . (($val === self::NULL_VALUE) ? "NULL" : intval($val));
							break;

						case EntityType::FLOAT:
						case EntityType::DOUBLE:
						case EntityType::DECIMAL:
							$value .= (($index > 0) ? ", " : "") . (($val === self::NULL_VALUE) ? "NULL" : "'" . addslashes($val) . "'");
							break;

						case EntityType::CHAR:
						case EntityType::VARCHAR:
						case EntityType::TEXT:
						case EntityType::MEDIUMTEXT:
						case EntityType::LONGTEXT:
						case EntityType::ENUM:
							$value .= (($index > 0) ? ", " : "") . (($val === self::NULL_VALUE) ? "NULL" : "'" . addslashes($val) . "'");
							break;

						default:
							$value .= (($index > 0) ? ", " : "") . (($val === self::NULL_VALUE) ? "NULL" : "'" . addslashes($val) . "'");
							break;
					}

					$index++;
				}
			}

			$query  = "INSERT " . (($useIgnoreMethod) ? " IGNORE " : "") . " INTO " . $entity->_getTableName() . " ";
			$query .= "(" . $field . ") VALUES (" . $value . ")";

			$result = $this->db->exec($query);

			if($result === FALSE)
			{
				$this->runtimeError = TRUE;
				$this->transactionClose();

				BXD::newException('TransactionError', 'Error execute query: ' . $query);
			}
		}
		else
		{
			BXD::newException('TransactionError', 'Transaction not STARTED. Execute the method transactionStart() and tray again.');
		}

		return $result;
	}

	/**
	 * Metodo che esegue un UPDATE nel Database prendendo in ingresso o una sola istanza
	 * della classe ENTITY oppure due istanze sempre del medesimo tipo. Nel primo caso
	 * viene costruita la query settando i valori prendendo come riferimento i campi della
	 * classe valorizzati. La clausola WHERE viene costruita solamente se esiste un campo
	 * SERIAL. Se non dovesse essere presente viene generato un errore. Nel secondo caso
	 * la prima ENTITY costruisce la clausola SET mentre la seconda costruisce la clausola
	 * WHERE fungendo da pattern di ricerca.
	 *
	 * @param Entity $entity Oggetto Entity da inserire nel DB
	 * @param Entity $pattern Oggetto Entity per costruire la clausola WHERE della query
	 *
	 * @example
	 *
	 *	Esempio di come utilizzare il metodo insert per aggiungere una semplice entity
	 *	NAME-SURNAME nella tabella USERS
	 *
	 *	$EntityUsers = new EntityUsers();
	 *	$EntityUsers->setId(1);
	 *	$EntityUsers->setName('Giuseppe');
	 *	$EntityUsers->setSurname('Rossi');
	 *
	 *	EntityManagerTransaction::getInstance()->save($EntityUsers);
	 *
	 * @return int Numero di righe effettivamente modificate
	 */

	public function save(Entity $entity, Entity $pattern = NULL)
	{
		if($this->openTransaction)
		{
			$query = "UPDATE " . (($pattern !== NULL) ? $pattern->_getTableName() : $entity->_getTableName()) . " SET ";

			$count = 0;

			foreach($entity->_getVars() as $name => $col)
			{
				$val = $entity->__get($name);

				if($pattern !== NULL || $entity->_getSerial() === NULL || ($entity->_getSerial() !== NULL && $entity->_getSerial('VAR_NAME') !== $name))
				{
					if(isset($val) || $val === self::NULL_VALUE)
					{
						$value = "";

						switch($col['TYPE'])
						{
							case EntityType::BIT:
							case EntityType::TINYINT:
							case EntityType::SMALLINT:
							case EntityType::MEDIUMINT:
							case EntityType::INT:
							case EntityType::BIGINT:
								$value = (($val === self::NULL_VALUE) ? "NULL" : intval($val));
								break;

							case EntityType::FLOAT:
							case EntityType::DOUBLE:
							case EntityType::DECIMAL:
								$value = (($val === self::NULL_VALUE) ? "NULL" : "'" . addslashes($val) . "'");
								break;

							case EntityType::CHAR:
							case EntityType::VARCHAR:
							case EntityType::TEXT:
							case EntityType::MEDIUMTEXT:
							case EntityType::LONGTEXT:
							case EntityType::ENUM:
								$value = (($val === self::NULL_VALUE) ? "NULL" : "'" . addslashes($val) . "'");
								break;

							default:
								$value = (($val === self::NULL_VALUE) ? "NULL" : "'" . addslashes($val) . "'");
								break;
						}

						if($count == 0)
						{
							$query .= "`" . $col['DB_COLUMN'] . "` = " . $value;
						}
						else
						{
							$query .= ", `" . $col['DB_COLUMN'] . "` = " . $value;
						}

						$count++;
					}
				}
			}

			if($count > 0)
			{
				$query .= " WHERE ";

				$serial = $entity->_getSerial();

				if($serial !== NULL && $pattern === NULL) // esiste una chiave primaria autoincrement
				{
					$query .= "`" . $serial["DB_COLUMN"] . "` = " . $entity->__get($serial["VAR_NAME"]);
				}
				else if(isset($pattern)) // usiamo il pattern per il riconoscimento delle tuple da aggiornare
				{
					$count = 0;

					foreach($pattern->_getVars() as $name => $col)
					{
						$val = $pattern->__get($name);

						if(isset($val))
						{
							$value = "";

							switch($col['TYPE'])
							{
								case EntityType::BIT:
								case EntityType::TINYINT:
								case EntityType::SMALLINT:
								case EntityType::MEDIUMINT:
								case EntityType::INT:
								case EntityType::BIGINT:
									$value = (($val === self::NULL_VALUE) ? "NULL" : intval($val));
									break;

								case EntityType::FLOAT:
								case EntityType::DOUBLE:
								case EntityType::DECIMAL:
									$value = (($val === self::NULL_VALUE) ? "NULL" : "'" . addslashes($val) . "'");
									break;

								case EntityType::CHAR:
								case EntityType::VARCHAR:
								case EntityType::TEXT:
								case EntityType::MEDIUMTEXT:
								case EntityType::LONGTEXT:
								case EntityType::ENUM:
									$value = (($val === self::NULL_VALUE) ? "NULL" : "'" . addslashes($val) . "'");
									break;

								default:
									$value = (($val === self::NULL_VALUE) ? "NULL" : "'" . addslashes($val) . "'");
									break;
							}

							if($count == 0)
							{
								$query .= "`" . $col['DB_COLUMN'] . "` " . (($val === self::NULL_VALUE) ? "IS" : "=") . " " . $value;
							}
							else
							{
								$query .= " AND `" . $col['DB_COLUMN'] . "` " . (($val === self::NULL_VALUE) ? "IS" : "=") . " " . $value;
							}

							$count++;
						}
					}

					if ($count == 0)
						$query .= " 1 ";
				}
			}
			
			$result = $this->db->exec($query);

			if($result === FALSE)
			{
				$this->runtimeError = TRUE;
				$this->transactionClose();

				BXD::newException('TransactionError', 'Error execute query: ' . $query);
			}
		}
		else
		{
			BXD::newException('TransactionError', 'Transaction not STARTED. Execute the method transactionStart() and tray again.');
		}
		
		return $result;
	}

	/**
	 * Restituisce l'ultimo ID inserito nel database.
	 *
	 * @return int Ultimo identificativo inserito
	 */

	public function getLastInsertId()
	{
		return $this->db->getLastInsertId();
	}

	/**
	 * Questo metodo è utilizzato per chiudere la transazione
	 */

	public function transactionClose($forceRollback = FALSE)
	{
		if($this->openTransaction)
		{
			if($this->runtimeError === TRUE)
			{
				$this->db->rollback();
			}
			else
			{
				if($forceRollback)
					$this->db->rollback();
				else
					$this->db->commit();
			}

			$this->openTransaction = FALSE;
		}
	}
}

?>
